﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using NUnit.Framework;
using PropertyExpression.Common;
using PropertyExpression.ControllableQuery;
using PropertyExpression.ControllableQuery.Test.Queries;

namespace PropertyExpression.ControllableQueryTool.Test
{
    [TestFixture]
    public class MainTest
    {
        [Test]
        public void Test4()
        {
            ProcessQueryType(typeof(PartLinkQuery));
        }

        [Test]
        public void Test5()
        {
            ProcessQueryType(typeof(ModelPublicationSearchQuery));
        }

        [Test]
        public void Test()
        {
            Test(
                GetAllTypes()
                );
        }

        private static void Test(IEnumerable<Type> types)
        {
            foreach (var type in types)
            {
                ProcessQueryType(type);
            }
        }

        private static void ProcessQueryType(Type type)
        {
            foreach (var @interface in type.GetInterfaces())
            {                    
                if (InterfaceIsGenericAndEquals(@interface, typeof(IQuery<>)))
                {
                    ProcessQueryType(type, @interface.GetGenericArguments().Single());
                }
            }
        }

        private static IEnumerable<Type> GetAllTypes()
        {
            return GetAllAssemblies().SelectMany(assembly => assembly.GetTypes());
        }

        private static IEnumerable<Assembly> GetAllAssemblies()
        {
            var assemblies = new HashSet<Assembly>();
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                assemblies.Add(assembly);
                LoadReferencedAssemblies(assembly, assemblies);
            }
            return assemblies;
        }

        private static void LoadReferencedAssemblies(Assembly assembly, HashSet<Assembly> assemblies)
        {
            foreach (var assemblyName in assembly.GetReferencedAssemblies())
            {
                var loadedAssembly = Assembly.Load(assemblyName.FullName);
                if (assemblies.Add(loadedAssembly))
                {
                    LoadReferencedAssemblies(loadedAssembly, assemblies);
                }
            }
        }

        private class ParamInfo
        {
            public readonly string Name;
            public readonly Func<ParamInfo, IOptionalValue<string>> SqlDeclareInfoFunc;
            public readonly Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> SqlTypeFunc;
            public readonly Type ParamType;

            public ParamInfo(
                string name, 
                Func<ParamInfo, IOptionalValue<string>> sqlDeclareInfoFunc, 
                Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> sqlTypeFunc,
                Type paramType)
            {
                Name = name;
                SqlDeclareInfoFunc = sqlDeclareInfoFunc;
                SqlTypeFunc = sqlTypeFunc;
                ParamType = paramType;
            }
        }

        private static void ProcessQueryType(Type queryType, Type dataInfoType)
        {
            Console.WriteLine(dataInfoType);
            Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
            foreach (var queryCreator in InvokeConstructor(queryType))
            {
                var paramInfoList = new List<ParamInfo>();
                var queryText = ((IQuery) queryCreator(new QueryCreatorContext(paramInfoList))).TransformText();

                var executionResult = WithSqlCommand(
                    sqlCommand =>
                        {
                            sqlCommand.CommandText = "SET SHOWPLAN_XML ON";
                            sqlCommand.ExecuteNonQuery();

                            var dictionary = new Dictionary<string, string>();
                            var result = ExecuteQuery(sqlCommand, queryText, paramInfoList, dictionary);

                            sqlCommand.CommandText = "SET SHOWPLAN_XML OFF";
                            sqlCommand.ExecuteNonQuery();

                            return new {result, dictionary};
                        }
                    );

                var xmlDocument = new XmlDocument();
                xmlDocument.LoadXml((string) executionResult.result);
                var xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);
                xmlNamespaceManager.AddNamespace("a", "http://schemas.microsoft.com/sqlserver/2004/07/showplan");

                //TODO:
                Console.WriteLine(queryText);
                Console.WriteLine("#################");

                var sqlParamInfoList = paramInfoList.Select(
                    paramInfo =>
                    new
                        {
                            paramInfo,
                            SqlType = GetSqlType(executionResult.dictionary, paramInfo.Name).Process(value => value, () => paramInfo.SqlTypeFunc(paramInfo, xmlDocument, xmlNamespaceManager))
                        }
                    );

                foreach (var sqlParamInfo in sqlParamInfoList)
                {
                    Console.WriteLine("{0} {1} {2}",
                                      sqlParamInfo.paramInfo.Name,
                                      sqlParamInfo.paramInfo.ParamType,
                                      sqlParamInfo.SqlType);
                }

                Console.WriteLine("****************************");


                WithSqlCommand(
                    sqlCommand =>
                        {
                            sqlCommand.CommandText = GetCommandText(
                                sqlParamInfoList.Select(
                                    sqlParamInfo => Tuple.Create(sqlParamInfo.paramInfo.Name, sqlParamInfo.SqlType)
                                    ),
                                queryText);

                            DataTable schemaTable;
                            using (var sqlDataReader = sqlCommand.ExecuteReader(CommandBehavior.SchemaOnly))
                            {
                                schemaTable = sqlDataReader.GetSchemaTable();
                            }
                            foreach (DataRow dataRow in schemaTable.Rows)
                            {
                                Console.WriteLine(@"{0} {1} '{2}'",
                                                  dataRow["ColumnName"],
                                                  dataRow["DataType"],
                                                  dataRow["AllowDBNull"]);
                            }
                        }
                    );

                Console.WriteLine("------------------------------");

                foreach (var propertyInfo in dataInfoType.GetProperties())
                {
                    Console.WriteLine("{0} {1}", propertyInfo.Name, propertyInfo.PropertyType);
                }

                Console.WriteLine("****************************");
            }
        }

        private static object ExecuteQuery(
            SqlCommand sqlCommand, string queryText, IEnumerable<ParamInfo> paramInfoList, Dictionary<string, string> dictionary)
        {
            var commandText = GetCommandText(
                paramInfoList.Select(
                    paramInfo => Tuple.Create(
                        paramInfo.Name,
                        paramInfo.SqlDeclareInfoFunc(paramInfo).Process(
                            value => value,
                            () => GetSqlType(dictionary, paramInfo.Name).Process(value => value, () => "VARBINARY(MAX)")
                            )
                                     )
                    ),
                queryText);
            sqlCommand.CommandText = commandText;
            try
            {
                return sqlCommand.ExecuteScalar();
            }
            catch (SqlException e)
            {
                if (e.Number == 1060)
                {
                    dictionary.Add(ToLines(commandText).ToArray()[e.LineNumber - 1], "bigint");
                    return ExecuteQuery(sqlCommand, queryText, paramInfoList, dictionary);
                }
                else
                {
                    throw;                    
                }                
            }
        }

        private static IOptionalValue<string> GetSqlType(Dictionary<string, string> dictionary, string name)
        {
            string value;
            return dictionary.TryGetValue(name, out value) ? value.AsOptionalValue() : OptionalValue.Nothing<string>();
        }

        private static IEnumerable<string> ToLines(string queryText)
        {
            using (var reader = new StringReader(queryText))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                    yield return line;
            }
        }

        private static string GetCommandText(IEnumerable<Tuple<string, string>> paramInfoList, string queryText)
        {
            var paramStringBuilder = new StringBuilder();
            foreach (var paramInfo in paramInfoList)
            {
                paramStringBuilder.AppendFormat("DECLARE {0} {1}", paramInfo.Item1, paramInfo.Item2);
                paramStringBuilder.AppendLine();
            }
            return string.Format(@"{0}
{1}",
                                 paramStringBuilder,
                                 queryText);
        }

        private static TResult WithSqlCommand<TResult>(Func<SqlCommand, TResult> func)
        {
            using (var connection = new SqlConnection(
                @"Data Source=(local);Initial Catalog=ControllableQuery;Integrated Security=True"))
            {
                connection.Open();

                using (var sqlCommand = new SqlCommand())
                {
                    sqlCommand.Connection = connection;

                    return func(sqlCommand);
                }
            }
        }

        private static void WithSqlCommand(Action<SqlCommand> action)
        {
            WithSqlCommand(
                command =>
                    {
                        action(command);
                        return new {};
                    }
                );
        }

        private delegate object QueryCreator(QueryCreatorContext paramList);

        private class QueryCreatorContext
        {
            public readonly List<ParamInfo> ParamInfos;
            private int sqlParameterIndex;

            public QueryCreatorContext(List<ParamInfo> paramInfos)
            {
                ParamInfos = paramInfos;
            }

            /// <summary>
            /// //TODO: этот метод скопипащев в аналогичный метод для алгоритма выполнения запроса. Устранить копипаст.
            /// </summary>
            /// <returns></returns>
            public string GetNextSqlParameterName()
            {
                var result = string.Format("p{0}", sqlParameterIndex);
                sqlParameterIndex++;
                return result;
            }
        }

        private static IEnumerable<IEnumerable<QueryCreator>> GetArgRepository<T>(
            IEnumerable<T> parameterInfos,
            Func<T, IEnumerable<QueryCreator>> choiceInstanceFunc)
        {
            return parameterInfos.Aggregate(
                new IEnumerable<QueryCreator>[] {}.AsEnumerable(),
                (current, parameterInfo) =>
                choiceInstanceFunc(parameterInfo).SelectMany(
                    choiceInstance => current.Any()
                                          ? current.Select(argList => argList.Concat(new[] {choiceInstance}))
                                          : new[] {new[] {choiceInstance}}
                    )
                );
        }

        private static Type MakeListType(Type itemType)
        {
            return typeof (List<>).MakeGenericType(itemType);
        }

        private static object GetList(Type listType)
        {
            return listType.GetConstructor(new Type[] {}).Invoke(new object[] {});
        }

        private static IEnumerable<QueryCreator> GetImplementationsForEnumerable(Type itemType, int count)
        {
            return GetArgRepository(
                Enumerable.Repeat(new {}, count),
                delegate
                    {
                        return InvokeConstructor(itemType);
                    }
                ).Select<IEnumerable<QueryCreator>, QueryCreator>(
                    queryCreatorEnumerable =>
                    paramList =>
                        {
                            var listType = MakeListType(itemType);
                            //TODO: по имени вызов метода через рефлекшен
                            var methodInfo = listType.GetMethod("Add");
                            var list = GetList(listType);
                            foreach (var item in queryCreatorEnumerable.Select(queryCreator => queryCreator(paramList)))
                            {
                                methodInfo.Invoke(list, new[] {item});
                            }
                            return list;
                        }
                );
        }

        /// <summary>
        /// //TODO: по имени вызов метода через рефлекшен
        /// </summary>
        private static readonly MethodInfo asOptionalValueMethodInfo = typeof(OptionalValue).GetMethod("AsOptionalValue");

        /// <summary>
        /// //TODO: по имени вызов метода через рефлекшен
        /// </summary>
        private static readonly MethodInfo nothingOptionalValueMethodInfo = typeof(OptionalValue).GetMethod("Nothing");

        private static IEnumerable<QueryCreator> GetChoiceInstances(Type type)
        {
            if (InterfaceIsGenericAndEquals(type, typeof(IOptionalValue<>)))
            {
                return new[] {asOptionalValueMethodInfo, nothingOptionalValueMethodInfo}.SelectMany(
                    genericMethodInfo =>
                        {
                            var methodInfo = genericMethodInfo.MakeGenericMethod(GetGenericArgument(type));
                            return InvokeMethodBase(methodInfo, objects => methodInfo.Invoke(null, objects));
                        }
                    );
            }
            else if (InterfaceIsGenericAndEquals(type, typeof(IParam<>)))
            {
                return new QueryCreator[]
                           {
                               paramList => GetConstructorInfo(typeof (Param<>).MakeGenericType(
                                   type.GetGenericArguments().ToArray())
                                                ).Invoke(new object[] {paramList})
                           };
            }
            else if (typeof(bool).Equals(type))
            {
                return new QueryCreator[] { delegate { return false; }, delegate { return true; }};
            }
            else if (InterfaceIsGenericAndEquals(type, typeof(IEnumerable<>)))
            {
                var itemType = type.GetGenericArguments().Single();

                return new[]
                           {
                               new QueryCreator[] {delegate { return GetList(MakeListType(itemType)); }},
                               //TODO: увеличить количество массивов для перебора
                               GetImplementationsForEnumerable(itemType, 1),
                               GetImplementationsForEnumerable(itemType, 2),
                               GetImplementationsForEnumerable(itemType, 3)
                           }.SelectMany(_ => _);
            }
            else if (type.IsInterface)
            {
                return GetImplementations(type).SelectMany(InvokeConstructor);
            }
            else
            {
                throw new ApplicationException();
            }
        }

        private static Type GetGenericArgument(Type type)
        {
            return type.GetGenericArguments().Single();
        }

        //TODO: кидать эксепшен, когда под двумя разными именами на сервер уходит один и тот же экземпляр IParam

        private class Param<T> : IParam<T>
        {
            private readonly QueryCreatorContext queryCreatorContext;

            public Param(QueryCreatorContext queryCreatorContext)
            {
                this.queryCreatorContext = queryCreatorContext;
            }

            public ISqlParam<T> GetSqlParam(string parameterName)
            {
                if (InterfaceIsGenericAndEquals(ParamType, typeof(IEnumerable<>)))
                {
                    //TODO: делать проверку, что SQL тип имеет одинаковый набор полей с .NET типом TItem.
                    var itemType = ParamType.GetGenericArguments().Single();
                    return GetSqlParam(
                        parameterName,
                        paramInfo => itemType.Name.AsOptionalValue(),
                        delegate { return itemType.Name; }
                        );
                }
                else
                {
                    return GetSqlParam(
                        parameterName,
                        paramInfo => OptionalValue.Nothing<string>(),
                        (paramInfo, xmlDocument, xmlNamespaceManager) =>
                        xmlDocument.SelectSingleNode(
                            string.Format(
                                @"//a:ColumnReference[@Column='{0}']/ancestor::a:Convert/@DataType",
                                paramInfo.Name),
                            xmlNamespaceManager).InnerText
                        );                    
                }
            }

            public ISqlParam<T> GenerateSqlParam()
            {
                return GetSqlParam(queryCreatorContext.GetNextSqlParameterName());
            }

            public override string ToString()
            {
                return this.ToSql();
            }

            private ISqlParam<T> GetSqlParam(
                string parameterName,
                Func<ParamInfo, IOptionalValue<string>> sqlDeclareInfoFunc,
                Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> sqlTypeFunc)
            {
                return new SqlParam(this, parameterName, sqlDeclareInfoFunc, sqlTypeFunc);
            }

            private class SqlParam : ISqlParam<T>
            {
                private readonly Param<T> parent;
                private readonly string parameterName;
                private readonly Func<ParamInfo, IOptionalValue<string>> sqlDeclareInfoFunc;
                private readonly Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> sqlTypeFunc;

                public SqlParam(
                    Param<T> parent, 
                    string parameterName,
                    Func<ParamInfo, IOptionalValue<string>> sqlDeclareInfoFunc,
                    Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> sqlTypeFunc)
                {
                    this.parent = parent;
                    this.parameterName = parameterName;
                    this.sqlDeclareInfoFunc = sqlDeclareInfoFunc;
                    this.sqlTypeFunc = sqlTypeFunc;
                }

                public string ToSql()
                {
                    return GetSql(name => string.Format("@{0}", name));
                }

                public string ToSql(int index)
                {
                    return GetSql(name => string.Format("@{0}{1}", name, index));
                }

                private string GetSql(Func<string, string> paramNameFunc)
                {
                    var paramName = paramNameFunc(parameterName);
                    parent.queryCreatorContext.ParamInfos.Add(
                        new ParamInfo(
                            paramName,
                            sqlDeclareInfoFunc,
                            sqlTypeFunc,
                            ParamType
                            )
                        );
                    //TODO: избавиться от копипаста: добавление @ идет и в алгоритме выполнения запроса
                    return string.Format(@"
{0}
",
                                         paramName);
                }

                public override string ToString()
                {
                    return ToSql();
                }
            }

            private static Type ParamType
            {
                get { return typeof(T); }
            }
        }

        private static IEnumerable<QueryCreator> InvokeConstructor(Type implementationType)
        {
            var constructorInfo = GetConstructorInfo(implementationType);
            return InvokeMethodBase(constructorInfo, constructorInfo.Invoke);
        }

        private static IEnumerable<QueryCreator> InvokeMethodBase(
            MethodBase methodBase, Func<object[], object> invokeFunc)
        {
            var parameterInfos = methodBase.GetParameters();
            if (parameterInfos.Length == 0)
            {
                return new QueryCreator[] { delegate { return invokeFunc(new object[] { }); } };
            }
            return GetArgRepository(parameterInfos, parameterInfo => GetChoiceInstances(parameterInfo.ParameterType))
                .Select<IEnumerable<QueryCreator>, QueryCreator>(
                    queryCreatorEnumerable => paramList => invokeFunc(
                        queryCreatorEnumerable.Select(queryCreator => queryCreator(paramList)).ToArray())
                );
        }

        private static IEnumerable<Type> GetImplementations(Type interfaceType)
        {
            foreach (var type in GetAllTypes())
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (interfaceType.IsGenericType)
                    {
                        if (@interface.IsGenericType)
                        {
                            if (interfaceType.GetGenericTypeDefinition().Equals(@interface.GetGenericTypeDefinition()))
                            {
                                var typeArguments = type.GetGenericArguments();
                                var dictionary = typeArguments.Select((type1, index) => new {type = type1, index})
                                    .ToDictionary(arg => arg.type, arg => arg.index);
                                var interfaceArguments = @interface.GetGenericArguments();
                                var inputInterfaceArguments = interfaceType.GetGenericArguments();
                                for (var index = 0; index < interfaceArguments.Length; index++)
                                {
                                    typeArguments[dictionary[interfaceArguments[index]]] = inputInterfaceArguments[index];
                                }
                                yield return type.MakeGenericType(typeArguments);
                            }
                        }
                    }
                    else
                    {
                        if (interfaceType.Equals(@interface))
                        {
                            yield return type;
                        }
                    }
                }
            }
        }

        private static ConstructorInfo GetConstructorInfo(Type type)
        {
            //TODO: написвать сообщение для эксепшена, если конструктор не один
            return type.GetConstructors().Single();
        }

        private static bool InterfaceIsGenericAndEquals(Type type1, Type type2)
        {
            return type1.IsGenericType && type2.Equals(type1.GetGenericTypeDefinition());
        }
    }
}